baubs git
Commit 30d87f82bcd24cfc6d83abd4a6415d15a1d94468
Parents : f94aa07
Author : Jan-Henrik Bruhn <hi@jhbruhn.de>
Date : 2025-12-05T23:25:55+01:00
Migrate to Tailwind CSS and fix canvas zoom issues
- Install Tailwind CSS and configure Vite plugin
- Replace all custom CSS classes with Tailwind utility classes
- Migrate all components to use Tailwind styling:
- App.tsx: Responsive layout with modern styling
- MachineConnection: Status badges and action buttons
- FileUpload: File input and progress bars with shimmer effect
- ProgressMonitor: Color blocks, state indicators, and actions
- ConfirmDialog: Modal overlay with backdrop blur
- PatternCanvas: Canvas viewer with floating controls
- Add custom shimmer animation for progress bars
- Fix canvas resizing issue during zoom operations:
- Add ResizeObserver for stable container dimensions
- Use clientWidth/clientHeight instead of offset dimensions
- Cache container size to prevent layout thrashing
- Improve zoom button behavior to zoom towards viewport center
- Maintain consistent design with shadows, borders, and transitions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changes
13 files changed, 950 insertions(+), 1042 deletions(-)
Diff
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..2206350
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,9 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(npm run build:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/.gitignore b/.gitignore
index a547bf3..4d94e69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+protocol/
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 24b57bf..b48b18d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,13 @@
"name": "web",
"version": "0.0.0",
"dependencies": {
+ "@tailwindcss/vite": "^4.1.17",
+ "konva": "^10.0.12",
"pyodide": "^0.27.4",
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-konva": "^19.2.1",
+ "tailwindcss": "^4.1.17"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -298,7 +302,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"optional": true,
"os": [
"aix"
@@ -314,7 +317,6 @@
"cpu": [
"arm"
],
- "dev": true,
"optional": true,
"os": [
"android"
@@ -330,7 +332,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"android"
@@ -346,7 +347,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"android"
@@ -362,7 +362,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"darwin"
@@ -378,7 +377,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"darwin"
@@ -394,7 +392,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"freebsd"
@@ -410,7 +407,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"freebsd"
@@ -426,7 +422,6 @@
"cpu": [
"arm"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -442,7 +437,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -458,7 +452,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -474,7 +467,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -490,7 +482,6 @@
"cpu": [
"mips64el"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -506,7 +497,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -522,7 +512,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -538,7 +527,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -554,7 +542,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -570,7 +557,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"netbsd"
@@ -586,7 +572,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"netbsd"
@@ -602,7 +587,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"openbsd"
@@ -618,7 +602,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"openbsd"
@@ -634,7 +617,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"openharmony"
@@ -650,7 +632,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"sunos"
@@ -666,7 +647,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"win32"
@@ -682,7 +662,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"optional": true,
"os": [
"win32"
@@ -698,7 +677,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"win32"
@@ -905,7 +883,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -915,7 +892,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -925,7 +901,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -933,14 +908,12 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -959,7 +932,6 @@
"cpu": [
"arm"
],
- "dev": true,
"optional": true,
"os": [
"android"
@@ -972,7 +944,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"android"
@@ -985,7 +956,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"darwin"
@@ -998,7 +968,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"darwin"
@@ -1011,7 +980,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"freebsd"
@@ -1024,7 +992,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"freebsd"
@@ -1037,7 +1004,6 @@
"cpu": [
"arm"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1050,7 +1016,6 @@
"cpu": [
"arm"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1063,7 +1028,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1076,7 +1040,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1089,7 +1052,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1102,7 +1064,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1115,7 +1076,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1128,7 +1088,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1141,7 +1100,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1154,7 +1112,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1167,7 +1124,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"linux"
@@ -1180,7 +1136,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"openharmony"
@@ -1193,7 +1148,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"optional": true,
"os": [
"win32"
@@ -1206,7 +1160,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"optional": true,
"os": [
"win32"
@@ -1219,7 +1172,6 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"win32"
@@ -1232,12 +1184,268 @@
"cpu": [
"x64"
],
- "dev": true,
"optional": true,
"os": [
"win32"
]
},
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
+ "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.17"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
+ "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-x64": "4.1.17",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.17",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.17",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.17",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
+ "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
+ "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
+ "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
+ "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
+ "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
+ "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
+ "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
+ "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
+ "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
+ "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.6.0",
+ "@emnapi/runtime": "^1.6.0",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.0.7",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
+ "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
+ "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz",
+ "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.1.17",
+ "@tailwindcss/oxide": "4.1.17",
+ "tailwindcss": "4.1.17"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1282,8 +1490,7 @@
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
@@ -1295,7 +1502,7 @@
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1304,7 +1511,6 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
- "dev": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1318,6 +1524,15 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/react-reconciler": {
+ "version": "0.32.3",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.3.tgz",
+ "integrity": "sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
@@ -1887,8 +2102,7 @@
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
},
"node_modules/debug": {
"version": "4.4.3",
@@ -1913,17 +2127,38 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.262",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
"dev": true
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -2177,7 +2412,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
"engines": {
"node": ">=12.0.0"
},
@@ -2253,7 +2487,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -2296,6 +2529,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -2408,6 +2647,36 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "node_modules/its-fine": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
+ "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.9"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/its-fine/node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2477,6 +2746,26 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/konva": {
+ "version": "10.0.12",
+ "resolved": "https://registry.npmjs.org/konva/-/konva-10.0.12.tgz",
+ "integrity": "sha512-DHmkeG5FbW6tLCkbMQTi1ihWycfzljrn0V7umUUuewxx7aoINcI71ksgBX9fTPNXhlsK4/JoMgKwI/iCde+BRw==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/lavrton"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/konva"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/lavrton"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2490,6 +2779,255 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lightningcss": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2520,6 +3058,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2542,7 +3089,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2669,14 +3215,12 @@
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -2688,7 +3232,6 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -2760,6 +3303,52 @@
"react": "^19.2.0"
}
},
+ "node_modules/react-konva": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.2.1.tgz",
+ "integrity": "sha512-sqZWCzQGpdMrU5aeunR0oxUY8UeCPbU8gnAYxMtAn6BT4coeSpiATKOctsoxRu6F56TAcF+s0c6Lul9ansNqQA==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/lavrton"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/konva"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/lavrton"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.32.3",
+ "its-fine": "^2.0.0",
+ "react-reconciler": "0.33.0",
+ "scheduler": "0.27.0"
+ },
+ "peerDependencies": {
+ "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ }
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz",
+ "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -2806,7 +3395,6 @@
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
- "dev": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -2882,7 +3470,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2911,11 +3498,29 @@
"node": ">=8"
}
},
+ "node_modules/tailwindcss": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "dev": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@@ -3003,7 +3608,7 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "dev": true
+ "devOptional": true
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
@@ -3048,7 +3653,6 @@
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
- "dev": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
diff --git a/package.json b/package.json
index aedf1ce..4458565 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
+ "@tailwindcss/vite": "^4.1.17",
+ "konva": "^10.0.12",
+ "pyodide": "^0.27.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "pyodide": "^0.27.4"
+ "react-konva": "^19.2.1",
+ "tailwindcss": "^4.1.17"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/src/App.css b/src/App.css
index 4fb4c6c..5eaacb4 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,763 +1,6 @@
-:root {
- --primary-color: #0066cc;
- --secondary-color: #6c757d;
- --danger-color: #dc3545;
- --success-color: #28a745;
- --warning-color: #ffc107;
- --info-color: #17a2b8;
- --background: #f5f5f5;
- --panel-background: #ffffff;
- --border-color: #dee2e6;
- --text-color: #212529;
- --text-muted: #6c757d;
-}
-
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- background-color: var(--background);
- color: var(--text-color);
-}
-
-.app {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-.app-header {
- background-color: var(--panel-background);
- padding: 1.5rem 2rem;
- border-bottom: 1px solid var(--border-color);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.app-header h1 {
- font-size: 1.75rem;
- font-weight: 600;
- margin-bottom: 0.5rem;
-}
-
-.error-message {
- background-color: #f8d7da;
- color: #721c24;
- padding: 0.75rem 1rem;
- border-radius: 4px;
- margin-top: 1rem;
- border: 1px solid #f5c6cb;
-}
-
-.app-content {
- flex: 1;
- display: grid;
- grid-template-columns: 400px 1fr;
- gap: 1.5rem;
- padding: 1.5rem;
- max-width: 1600px;
- width: 100%;
- margin: 0 auto;
-}
-
-.left-panel {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.right-panel {
- display: flex;
- flex-direction: column;
-}
-
-.connection-panel,
-.file-upload-panel,
-.progress-panel,
-.canvas-panel {
- background-color: var(--panel-background);
- padding: 1.5rem;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-h2 {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 1rem;
- padding-bottom: 0.5rem;
- border-bottom: 2px solid var(--border-color);
-}
-
-h3 {
- font-size: 1rem;
- font-weight: 600;
- margin: 1rem 0 0.5rem 0;
-}
-
-.detail-row {
- display: flex;
- justify-content: space-between;
- padding: 0.5rem 0;
- border-bottom: 1px solid var(--border-color);
-}
-
-.detail-row:last-child {
- border-bottom: none;
-}
-
-.detail-row .label {
- font-weight: 500;
- color: var(--text-muted);
-}
-
-.detail-row .value {
- font-weight: 600;
-}
-
-.status-bar {
- display: flex;
- align-items: center;
- gap: 1rem;
- margin-bottom: 1rem;
- padding: 0.75rem;
- background-color: var(--background);
- border-radius: 4px;
-}
-
-.status-indicator {
- padding: 0.5rem 1rem;
- border-radius: 4px;
- font-weight: 600;
- font-size: 0.875rem;
- text-transform: uppercase;
-}
-
-.status-indicator.status-16 { /* IDLE */
- background-color: #d1ecf1;
- color: #0c5460;
-}
-
-.status-indicator.status-17 { /* SEWING_WAIT */
- background-color: #d4edda;
- color: #155724;
-}
-
-.status-indicator.status-48 { /* SEWING */
- background-color: #fff3cd;
- color: #856404;
-}
-
-.status-indicator.status-49 { /* SEWING_COMPLETE */
- background-color: #d4edda;
- color: #155724;
-}
-
-.status-indicator.status-64 { /* COLOR_CHANGE_WAIT */
- background-color: #fff3cd;
- color: #856404;
-}
-
-.error-indicator {
- background-color: #f8d7da;
- color: #721c24;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- font-weight: 600;
- font-size: 0.875rem;
-}
-
-.polling-indicator {
- color: var(--primary-color);
- font-size: 0.75rem;
- animation: pulse 1s ease-in-out infinite;
-}
-
-@keyframes pulse {
- 0%, 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.3;
- }
-}
-
-.connection-actions,
-.progress-actions {
- display: flex;
- gap: 0.75rem;
- margin-top: 1rem;
- flex-wrap: wrap;
-}
-
-button {
- padding: 0.75rem 1.5rem;
- border: none;
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.btn-primary {
- background-color: var(--primary-color);
- color: white;
-}
-
-.btn-primary:hover:not(:disabled) {
- background-color: #0052a3;
-}
-
-.btn-secondary {
- background-color: var(--secondary-color);
- color: white;
-}
-
-.btn-secondary:hover:not(:disabled) {
- background-color: #5a6268;
-}
-
-.btn-danger {
- background-color: var(--danger-color);
- color: white;
-}
-
-.btn-danger:hover:not(:disabled) {
- background-color: #c82333;
-}
-
-.file-input {
- display: none;
-}
-
-.progress-bar {
- height: 8px;
- background-color: var(--border-color);
- border-radius: 4px;
- overflow: hidden;
- margin: 1rem 0;
-}
-
-.progress-fill {
- height: 100%;
- background-color: var(--primary-color);
- transition: width 0.3s;
-}
-
-/* Canvas container with Konva */
-.canvas-container {
- position: relative;
- width: 100%;
- height: 600px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background-color: #fafafa;
- overflow: hidden;
-}
-
-.canvas-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 600px;
- color: var(--text-muted);
- font-style: italic;
-}
-
-/* Canvas overlay elements */
-.canvas-legend {
- position: absolute;
- top: 10px;
- left: 10px;
- background: rgba(255, 255, 255, 0.95);
- padding: 12px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- z-index: 10;
- backdrop-filter: blur(4px);
- max-width: 150px;
-}
-
-.canvas-legend h4 {
- margin: 0 0 8px 0;
- font-size: 13px;
- font-weight: 600;
- color: var(--text-color);
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 6px;
-}
-
-.legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 6px;
-}
-
-.legend-item:last-child {
- margin-bottom: 0;
-}
-
-.legend-swatch {
- width: 20px;
- height: 20px;
- border-radius: 3px;
- border: 1px solid #000;
- flex-shrink: 0;
-}
-
-.legend-label {
- font-size: 12px;
- color: var(--text-color);
-}
-
-.canvas-dimensions {
- position: absolute;
- bottom: 165px;
- right: 20px;
- background: rgba(255, 255, 255, 0.95);
- padding: 8px 16px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- z-index: 11;
- backdrop-filter: blur(4px);
- font-size: 14px;
- font-weight: 600;
- color: var(--text-color);
-}
-
-.canvas-offset-info {
- position: absolute;
- bottom: 80px;
- right: 20px;
- background: rgba(255, 255, 255, 0.95);
- padding: 10px 14px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- z-index: 11;
- backdrop-filter: blur(4px);
- min-width: 180px;
-}
-
-.offset-label {
- font-size: 11px;
- font-weight: 600;
- color: var(--text-muted);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 4px;
-}
-
-.offset-value {
- font-size: 13px;
- font-weight: 600;
- color: var(--primary-color);
- margin-bottom: 4px;
-}
-
-.offset-hint {
- font-size: 10px;
- color: var(--text-muted);
- font-style: italic;
-}
-
-/* Zoom controls */
-.zoom-controls {
- position: absolute;
- bottom: 20px;
- right: 20px;
- display: flex;
- gap: 8px;
- align-items: center;
- background: rgba(255, 255, 255, 0.95);
- padding: 8px 12px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- z-index: 10;
- backdrop-filter: blur(4px);
-}
-
-.zoom-btn {
- width: 32px;
- height: 32px;
- padding: 0;
- font-size: 18px;
- font-weight: bold;
- border: 1px solid var(--border-color);
- background: white;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.zoom-btn:hover:not(:disabled) {
- background: var(--primary-color);
- color: white;
- border-color: var(--primary-color);
- transform: translateY(-1px);
- box-shadow: 0 2px 6px rgba(0, 102, 204, 0.3);
-}
-
-.zoom-btn:active:not(:disabled) {
- transform: translateY(0);
-}
-
-.zoom-level {
- min-width: 50px;
- text-align: center;
- font-size: 13px;
- font-weight: 600;
- color: var(--text-color);
- user-select: none;
-}
-
-.zoom-reset {
- margin-left: 4px;
- font-size: 20px;
-}
-
-.status-message {
- padding: 1rem;
- border-radius: 4px;
- margin: 1rem 0;
- font-weight: 500;
-}
-
-.status-message.success {
- background-color: #d4edda;
- color: #155724;
- border: 1px solid #c3e6cb;
-}
-
-.status-message.warning {
- background-color: #fff3cd;
- color: #856404;
- border: 1px solid #ffeeba;
-}
-
-.status-message.info {
- background-color: #d1ecf1;
- color: #0c5460;
- border: 1px solid #bee5eb;
-}
-
-/* Color Block Progress Styles */
-.color-blocks {
- margin-top: 1.5rem;
- padding-top: 1rem;
- border-top: 1px solid var(--border-color);
-}
-
-.color-block-list {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.color-block-item {
- padding: 0.75rem;
- border-radius: 4px;
- background-color: var(--background);
- border: 2px solid transparent;
- transition: all 0.3s;
-}
-
-.color-block-item.completed {
- border-color: var(--success-color);
- background-color: #f0f9f4;
-}
-
-.color-block-item.current {
- border-color: var(--primary-color);
- background-color: #e7f3ff;
- box-shadow: 0 2px 8px rgba(0, 102, 204, 0.2);
-}
-
-.color-block-item.pending {
- opacity: 0.6;
-}
-
-.block-header {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.color-swatch {
- width: 24px;
- height: 24px;
- border-radius: 4px;
- border: 2px solid var(--border-color);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.block-label {
- font-weight: 600;
- flex: 1;
-}
-
-.block-status {
- font-size: 1.25rem;
- font-weight: bold;
- color: var(--text-muted);
-}
-
-.color-block-item.completed .block-status {
- color: var(--success-color);
-}
-
-.color-block-item.current .block-status {
- color: var(--primary-color);
-}
-
-.block-stitches {
- font-size: 0.875rem;
- color: var(--text-muted);
-}
-
-.block-progress-bar {
- margin-top: 0.5rem;
- height: 4px;
- background-color: #fff;
- border-radius: 2px;
- overflow: hidden;
-}
-
-.block-progress-fill {
- height: 100%;
- background-color: var(--primary-color);
- transition: width 0.3s;
-}
-
-@media (max-width: 1024px) {
- .app-content {
- grid-template-columns: 1fr;
- }
-}
-
-/* ==========================================================================
- State-Based UX Safety Styles
- ========================================================================== */
-
-/* Confirmation Dialog Styles */
-.confirm-dialog-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
-}
-
-.confirm-dialog {
- background-color: var(--panel-background);
- border-radius: 8px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
- max-width: 500px;
- width: 90%;
- margin: 1rem;
-}
-
-.confirm-dialog-danger {
- border-top: 4px solid #dc3545;
-}
-
-.confirm-dialog-warning {
- border-top: 4px solid #ffc107;
-}
-
-.confirm-dialog-header {
- padding: 1.5rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.confirm-dialog-header h3 {
- margin: 0;
- font-size: 1.25rem;
- font-weight: 600;
-}
-
-.confirm-dialog-body {
- padding: 1.5rem;
-}
-
-.confirm-dialog-body p {
- margin: 0;
- line-height: 1.5;
- color: var(--text-color);
-}
-
-.confirm-dialog-actions {
- padding: 1rem 1.5rem;
- display: flex;
- gap: 0.75rem;
- justify-content: flex-end;
- border-top: 1px solid var(--border-color);
-}
-
-/* Enhanced Status Badge */
-.status-badge {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- font-weight: 600;
- font-size: 0.875rem;
-}
-
-.status-badge-idle,
-.status-badge-info {
- background-color: #d1ecf1;
- color: #0c5460;
- border: 1px solid #bee5eb;
-}
-
-.status-badge-active,
-.status-badge-waiting,
-.status-badge-warning {
- background-color: #fff3cd;
- color: #856404;
- border: 1px solid #ffeeba;
-}
-
-.status-badge-complete,
-.status-badge-success {
- background-color: #d4edda;
- color: #155724;
- border: 1px solid #c3e6cb;
-}
-
-.status-badge-interrupted,
-.status-badge-error,
-.status-badge-danger {
- background-color: #f8d7da;
- color: #721c24;
- border: 1px solid #f5c6cb;
-}
-
-.status-icon {
- font-size: 1.1rem;
- line-height: 1;
-}
-
-.status-text {
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-/* State Indicator Component */
-.state-indicator {
- display: flex;
- align-items: center;
- gap: 1rem;
- padding: 1rem;
- border-radius: 8px;
- margin: 1rem 0;
- border-left: 4px solid transparent;
-}
-
-.state-indicator-idle,
-.state-indicator-info {
- background-color: #e7f3ff;
- border-left-color: #0066cc;
-}
-
-.state-indicator-active,
-.state-indicator-waiting,
-.state-indicator-warning {
- background-color: #fff8e1;
- border-left-color: #ffc107;
-}
-
-.state-indicator-complete,
-.state-indicator-success {
- background-color: #e8f5e9;
- border-left-color: #28a745;
-}
-
-.state-indicator-interrupted,
-.state-indicator-error,
-.state-indicator-danger {
- background-color: #ffebee;
- border-left-color: #dc3545;
-}
-
-.state-icon {
- font-size: 2rem;
- line-height: 1;
-}
-
-.state-info {
- flex: 1;
-}
-
-.state-label {
- font-weight: 600;
- font-size: 1rem;
- margin-bottom: 0.25rem;
-}
-
-.state-description {
- font-size: 0.875rem;
- color: #666;
-}
-
-/* Enhanced Progress Visualization */
-.progress-bar {
- height: 12px;
- background-color: var(--border-color);
- border-radius: 6px;
- overflow: hidden;
- margin: 1rem 0;
- position: relative;
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.progress-fill {
- height: 100%;
- background: linear-gradient(90deg, var(--primary-color), #0052a3);
- transition: width 0.3s ease;
- position: relative;
- overflow: hidden;
-}
-
-.progress-fill::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(255, 255, 255, 0.3),
- transparent
- );
- animation: shimmer 2s infinite;
-}
+@import "tailwindcss";
+/* Custom animations for shimmer effect */
@keyframes shimmer {
0% {
transform: translateX(-100%);
@@ -766,38 +9,3 @@ button:disabled {
transform: translateX(100%);
}
}
-
-/* Button enhancements for safety */
-button.btn-danger:hover:not(:disabled) {
- background-color: #c82333;
- box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
- transform: translateY(-1px);
-}
-
-button.btn-primary:hover:not(:disabled) {
- box-shadow: 0 2px 8px rgba(0, 102, 204, 0.3);
- transform: translateY(-1px);
-}
-
-button.btn-secondary:hover:not(:disabled) {
- box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3);
- transform: translateY(-1px);
-}
-
-button:active:not(:disabled) {
- transform: translateY(0);
-}
-
-/* Info message styles */
-.status-message.info {
- background-color: #d1ecf1;
- color: #0c5460;
- border: 1px solid #bee5eb;
-}
-
-/* Enhanced visual feedback for disabled state */
-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- filter: grayscale(0.3);
-}
diff --git a/src/App.tsx b/src/App.tsx
index 68f7368..61f83f0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -49,22 +49,22 @@ function App() {
}, []);
return (
- <div className="app">
- <header className="app-header">
- <h1>Brother Embroidery Machine Controller</h1>
+ <div className="min-h-screen flex flex-col">
+ <header className="bg-white px-8 py-6 border-b border-gray-300 shadow-md">
+ <h1 className="text-3xl font-semibold mb-2">Brother Embroidery Machine Controller</h1>
{machine.error && (
- <div className="error-message">{machine.error}</div>
+ <div className="bg-red-100 text-red-900 px-4 py-3 rounded border border-red-200 mt-4">{machine.error}</div>
)}
{pyodideError && (
- <div className="error-message">Python Error: {pyodideError}</div>
+ <div className="bg-red-100 text-red-900 px-4 py-3 rounded border border-red-200 mt-4">Python Error: {pyodideError}</div>
)}
{!pyodideReady && !pyodideError && (
- <div className="info-message">Initializing Python environment...</div>
+ <div className="bg-blue-100 text-blue-900 px-4 py-3 rounded border border-blue-200 mt-4">Initializing Python environment...</div>
)}
</header>
- <div className="app-content">
- <div className="left-panel">
+ <div className="flex-1 grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6 p-6 max-w-[1600px] w-full mx-auto">
+ <div className="flex flex-col gap-6">
<MachineConnection
isConnected={machine.isConnected}
machineInfo={machine.machineInfo}
@@ -101,7 +101,7 @@ function App() {
/>
</div>
- <div className="right-panel">
+ <div className="flex flex-col">
<PatternCanvas
pesData={pesData}
sewingProgress={machine.sewingProgress}
diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx
index 9fb27a2..e0fff6d 100644
--- a/src/components/ConfirmDialog.tsx
+++ b/src/components/ConfirmDialog.tsx
@@ -38,31 +38,31 @@ export function ConfirmDialog({
if (!isOpen) return null;
return (
- <div className="confirm-dialog-overlay" onClick={onCancel}>
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[1000]" onClick={onCancel}>
<div
- className={`confirm-dialog confirm-dialog-${variant}`}
+ className={`bg-white rounded-lg shadow-2xl max-w-lg w-[90%] m-4 ${variant === 'danger' ? 'border-t-4 border-red-600' : 'border-t-4 border-yellow-500'}`}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-message"
>
- <div className="confirm-dialog-header">
- <h3 id="dialog-title">{title}</h3>
+ <div className="p-6 border-b border-gray-300">
+ <h3 id="dialog-title" className="m-0 text-xl font-semibold">{title}</h3>
</div>
- <div className="confirm-dialog-body">
- <p id="dialog-message">{message}</p>
+ <div className="p-6">
+ <p id="dialog-message" className="m-0 leading-relaxed text-gray-900">{message}</p>
</div>
- <div className="confirm-dialog-actions">
+ <div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300">
<button
onClick={onCancel}
- className="btn-secondary"
+ className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]"
autoFocus
>
{cancelText}
</button>
<button
onClick={onConfirm}
- className={variant === 'danger' ? 'btn-danger' : 'btn-primary'}
+ className={variant === 'danger' ? 'px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]' : 'px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]'}
>
{confirmText}
</button>
diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx
index 5a172fa..b2dcccc 100644
--- a/src/components/FileUpload.tsx
+++ b/src/components/FileUpload.tsx
@@ -62,43 +62,43 @@ export function FileUpload({
}, [pesData, fileName, onUpload, patternOffset]);
return (
- <div className="file-upload-panel">
- <h2>Pattern File</h2>
+ <div className="bg-white p-6 rounded-lg shadow-md">
+ <h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern File</h2>
- <div className="upload-controls">
+ <div>
<input
type="file"
accept=".pes"
onChange={handleFileChange}
id="file-input"
- className="file-input"
+ className="hidden"
disabled={!pyodideReady || isLoading}
/>
- <label htmlFor="file-input" className={`btn-secondary ${!pyodideReady || isLoading ? 'disabled' : ''}`}>
+ <label htmlFor="file-input" className={`inline-block px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm cursor-pointer transition-all ${!pyodideReady || isLoading ? 'opacity-50 cursor-not-allowed grayscale-[0.3]' : 'hover:bg-gray-700 hover:shadow-md hover:-translate-y-0.5 active:translate-y-0'}`}>
{isLoading ? 'Loading...' : !pyodideReady ? 'Initializing...' : 'Choose PES File'}
</label>
{pesData && (
- <div className="pattern-info">
- <h3>Pattern Details</h3>
- <div className="detail-row">
- <span className="label">Total Stitches:</span>
- <span className="value">{pesData.stitchCount}</span>
+ <div className="mt-4">
+ <h3 className="text-base font-semibold my-4">Pattern Details</h3>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Total Stitches:</span>
+ <span className="font-semibold">{pesData.stitchCount}</span>
</div>
- <div className="detail-row">
- <span className="label">Colors:</span>
- <span className="value">{pesData.colorCount}</span>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Colors:</span>
+ <span className="font-semibold">{pesData.colorCount}</span>
</div>
- <div className="detail-row">
- <span className="label">Size:</span>
- <span className="value">
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Size:</span>
+ <span className="font-semibold">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span>
</div>
- <div className="detail-row">
- <span className="label">Bounds:</span>
- <span className="value">
+ <div className="flex justify-between py-2">
+ <span className="font-medium text-gray-600">Bounds:</span>
+ <span className="font-semibold">
({pesData.bounds.minX}, {pesData.bounds.minY}) to (
{pesData.bounds.maxX}, {pesData.bounds.maxY})
</span>
@@ -110,7 +110,7 @@ export function FileUpload({
<button
onClick={handleUpload}
disabled={!isConnected || uploadProgress > 0}
- className="btn-primary"
+ className="mt-4 px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]"
>
{uploadProgress > 0
? `Uploading... ${uploadProgress.toFixed(0)}%`
@@ -119,15 +119,15 @@ export function FileUpload({
)}
{pesData && !canUploadPattern(machineStatus) && (
- <div className="status-message warning">
+ <div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded border border-yellow-200 my-4 font-medium">
Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
</div>
)}
{uploadProgress > 0 && uploadProgress < 100 && (
- <div className="progress-bar">
+ <div className="h-3 bg-gray-300 rounded-md overflow-hidden my-4 shadow-inner">
<div
- className="progress-fill"
+ className="h-full bg-gradient-to-r from-blue-600 to-blue-700 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]"
style={{ width: `${uploadProgress}%` }}
/>
</div>
diff --git a/src/components/MachineConnection.tsx b/src/components/MachineConnection.tsx
index d2507dd..198ef59 100644
--- a/src/components/MachineConnection.tsx
+++ b/src/components/MachineConnection.tsx
@@ -49,70 +49,83 @@ export function MachineConnection({
const stateVisual = getStateVisualInfo(machineStatus);
+ const statusBadgeColors = {
+ idle: 'bg-cyan-100 text-cyan-800 border-cyan-200',
+ info: 'bg-cyan-100 text-cyan-800 border-cyan-200',
+ active: 'bg-yellow-100 text-yellow-800 border-yellow-200',
+ waiting: 'bg-yellow-100 text-yellow-800 border-yellow-200',
+ warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
+ complete: 'bg-green-100 text-green-800 border-green-200',
+ success: 'bg-green-100 text-green-800 border-green-200',
+ interrupted: 'bg-red-100 text-red-800 border-red-200',
+ error: 'bg-red-100 text-red-800 border-red-200',
+ danger: 'bg-red-100 text-red-800 border-red-200',
+ };
+
return (
- <div className="connection-panel">
- <h2>Machine Connection</h2>
+ <div className="bg-white p-6 rounded-lg shadow-md">
+ <h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Machine Connection</h2>
{!isConnected ? (
- <div className="connection-actions">
- <button onClick={onConnect} className="btn-primary">
+ <div className="flex gap-3 mt-4 flex-wrap">
+ <button onClick={onConnect} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Connect to Machine
</button>
</div>
) : (
- <div className="connection-info">
- <div className="status-bar">
- <span className={`status-badge status-badge-${stateVisual.color}`}>
- <span className="status-icon">{stateVisual.icon}</span>
- <span className="status-text">{machineStatusName}</span>
+ <div>
+ <div className="flex items-center gap-4 mb-4 p-3 bg-gray-100 rounded">
+ <span className={`flex items-center gap-2 px-4 py-2 rounded font-semibold text-sm border ${statusBadgeColors[stateVisual.color as keyof typeof statusBadgeColors] || statusBadgeColors.info}`}>
+ <span className="text-lg leading-none">{stateVisual.icon}</span>
+ <span className="uppercase tracking-wide">{machineStatusName}</span>
</span>
{isPolling && (
- <span className="polling-indicator" title="Polling machine status">●</span>
+ <span className="text-blue-600 text-xs animate-pulse" title="Polling machine status">●</span>
)}
{hasError(machineError) && (
- <span className="error-indicator">{getErrorMessage(machineError)}</span>
+ <span className="bg-red-100 text-red-900 px-4 py-2 rounded font-semibold text-sm">{getErrorMessage(machineError)}</span>
)}
</div>
{machineInfo && (
- <div className="machine-details">
- <div className="detail-row">
- <span className="label">Model:</span>
- <span className="value">{machineInfo.modelNumber}</span>
+ <div>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Model:</span>
+ <span className="font-semibold">{machineInfo.modelNumber}</span>
</div>
- <div className="detail-row">
- <span className="label">Serial:</span>
- <span className="value">{machineInfo.serialNumber}</span>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Serial:</span>
+ <span className="font-semibold">{machineInfo.serialNumber}</span>
</div>
- <div className="detail-row">
- <span className="label">Software:</span>
- <span className="value">{machineInfo.softwareVersion}</span>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Software:</span>
+ <span className="font-semibold">{machineInfo.softwareVersion}</span>
</div>
- <div className="detail-row">
- <span className="label">Max Area:</span>
- <span className="value">
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Max Area:</span>
+ <span className="font-semibold">
{(machineInfo.maxWidth / 10).toFixed(1)} x{' '}
{(machineInfo.maxHeight / 10).toFixed(1)} mm
</span>
</div>
- <div className="detail-row">
- <span className="label">MAC:</span>
- <span className="value">{machineInfo.macAddress}</span>
+ <div className="flex justify-between py-2">
+ <span className="font-medium text-gray-600">MAC:</span>
+ <span className="font-semibold">{machineInfo.macAddress}</span>
</div>
</div>
)}
{resumeAvailable && resumeFileName && (
- <div className="status-message success">
+ <div className="bg-green-100 text-green-800 px-4 py-3 rounded border border-green-200 my-4 font-medium">
Loaded cached pattern: "{resumeFileName}"
</div>
)}
- <div className="connection-actions">
- <button onClick={onRefresh} className="btn-secondary">
+ <div className="flex gap-3 mt-4 flex-wrap">
+ <button onClick={onRefresh} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Refresh Status
</button>
- <button onClick={handleDisconnectClick} className="btn-danger">
+ <button onClick={handleDisconnectClick} className="px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Disconnect
</button>
</div>
diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx
index b48f1c2..b76823d 100644
--- a/src/components/PatternCanvas.tsx
+++ b/src/components/PatternCanvas.tsx
@@ -28,26 +28,46 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
const [stageScale, setStageScale] = useState(1);
const [patternOffset, setPatternOffset] = useState({ x: 0, y: 0 });
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const initialScaleRef = useRef<number>(1);
+ // Track container size
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ const updateSize = () => {
+ if (containerRef.current) {
+ const width = containerRef.current.clientWidth;
+ const height = containerRef.current.clientHeight;
+ setContainerSize({ width, height });
+ }
+ };
+
+ // Initial size
+ updateSize();
+
+ // Watch for resize
+ const resizeObserver = new ResizeObserver(updateSize);
+ resizeObserver.observe(containerRef.current);
+
+ return () => resizeObserver.disconnect();
+ }, []);
+
// Calculate initial scale when pattern or hoop changes
useEffect(() => {
- if (!pesData || !containerRef.current) return;
+ if (!pesData || containerSize.width === 0) return;
const { bounds } = pesData;
const viewWidth = machineInfo ? machineInfo.maxWidth : bounds.maxX - bounds.minX;
const viewHeight = machineInfo ? machineInfo.maxHeight : bounds.maxY - bounds.minY;
- const width = containerRef.current.offsetWidth;
- const height = containerRef.current.offsetHeight;
-
- const initialScale = calculateInitialScale(width, height, viewWidth, viewHeight);
+ const initialScale = calculateInitialScale(containerSize.width, containerSize.height, viewWidth, viewHeight);
initialScaleRef.current = initialScale;
// Set initial scale and center position when pattern loads
setStageScale(initialScale);
- setStagePos({ x: width / 2, y: height / 2 });
- }, [pesData, machineInfo]);
+ setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
+ }, [pesData, machineInfo, containerSize]);
// Wheel zoom handler
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
@@ -84,21 +104,54 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
// Zoom control handlers
const handleZoomIn = useCallback(() => {
- const newScale = Math.min(stageScale * 1.2, 10);
+ const oldScale = stageScale;
+ const newScale = Math.min(oldScale * 1.2, 10);
+
+ // Zoom towards center of viewport
+ const centerX = containerSize.width / 2;
+ const centerY = containerSize.height / 2;
+
+ const mousePointTo = {
+ x: (centerX - stagePos.x) / oldScale,
+ y: (centerY - stagePos.y) / oldScale,
+ };
+
+ const newPos = {
+ x: centerX - mousePointTo.x * newScale,
+ y: centerY - mousePointTo.y * newScale,
+ };
+
setStageScale(newScale);
- }, [stageScale]);
+ setStagePos(newPos);
+ }, [stageScale, stagePos, containerSize]);
const handleZoomOut = useCallback(() => {
- const newScale = Math.max(stageScale / 1.2, 0.1);
+ const oldScale = stageScale;
+ const newScale = Math.max(oldScale / 1.2, 0.1);
+
+ // Zoom towards center of viewport
+ const centerX = containerSize.width / 2;
+ const centerY = containerSize.height / 2;
+
+ const mousePointTo = {
+ x: (centerX - stagePos.x) / oldScale,
+ y: (centerY - stagePos.y) / oldScale,
+ };
+
+ const newPos = {
+ x: centerX - mousePointTo.x * newScale,
+ y: centerY - mousePointTo.y * newScale,
+ };
+
setStageScale(newScale);
- }, [stageScale]);
+ setStagePos(newPos);
+ }, [stageScale, stagePos, containerSize]);
const handleZoomReset = useCallback(() => {
- if (!containerRef.current) return;
const initialScale = initialScaleRef.current;
setStageScale(initialScale);
- setStagePos({ x: containerRef.current.offsetWidth / 2, y: containerRef.current.offsetHeight / 2 });
- }, []);
+ setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
+ }, [containerSize]);
// Pattern drag handlers
const handlePatternDragEnd = useCallback((e: Konva.KonvaEventObject<DragEvent>) => {
@@ -169,13 +222,13 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
}, [renderPatternLayer]);
return (
- <div className="canvas-panel">
- <h2>Pattern Preview</h2>
- <div className="canvas-container" ref={containerRef} style={{ width: '100%', height: '600px' }}>
- {containerRef.current && (
+ <div className="bg-white p-6 rounded-lg shadow-md">
+ <h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern Preview</h2>
+ <div className="relative w-full h-[600px] border border-gray-300 rounded bg-gray-50 overflow-hidden" ref={containerRef}>
+ {containerSize.width > 0 && (
<Stage
- width={containerRef.current.offsetWidth}
- height={containerRef.current.offsetHeight}
+ width={containerSize.width}
+ height={containerSize.height}
x={stagePos.x}
y={stagePos.y}
scaleX={stageScale}
@@ -235,7 +288,7 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
{/* Placeholder overlay when no pattern is loaded */}
{!pesData && (
- <div className="canvas-placeholder">
+ <div className="flex items-center justify-center h-[600px] text-gray-600 italic">
Load a PES file to preview the pattern
</div>
)}
@@ -244,46 +297,46 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, onPatternO
{pesData && (
<>
{/* Thread Legend Overlay */}
- <div className="canvas-legend">
- <h4>Threads</h4>
+ <div className="absolute top-2.5 left-2.5 bg-white/95 backdrop-blur-sm p-3 rounded-lg shadow-lg z-10 max-w-[150px]">
+ <h4 className="m-0 mb-2 text-[13px] font-semibold text-gray-900 border-b border-gray-300 pb-1.5">Threads</h4>
{pesData.threads.map((thread, index) => (
- <div key={index} className="legend-item">
+ <div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
<div
- className="legend-swatch"
+ className="w-5 h-5 rounded border border-black flex-shrink-0"
style={{ backgroundColor: thread.hex }}
/>
- <span className="legend-label">Thread {index + 1}</span>
+ <span className="text-xs text-gray-900">Thread {index + 1}</span>
</div>
))}
</div>
{/* Pattern Dimensions Overlay */}
- <div className="canvas-dimensions">
+ <div className="absolute bottom-[165px] right-5 bg-white/95 backdrop-blur-sm px-4 py-2 rounded-lg shadow-lg z-[11] text-sm font-semibold text-gray-900">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</div>
{/* Pattern Offset Indicator */}
- <div className="canvas-offset-info">
- <div className="offset-label">Pattern Position:</div>
- <div className="offset-value">
+ <div className="absolute bottom-20 right-5 bg-white/95 backdrop-blur-sm p-2.5 px-3.5 rounded-lg shadow-lg z-[11] min-w-[180px]">
+ <div className="text-[11px] font-semibold text-gray-600 uppercase tracking-wider mb-1">Pattern Position:</div>
+ <div className="text-[13px] font-semibold text-blue-600 mb-1">
X: {(patternOffset.x / 10).toFixed(1)}mm, Y: {(patternOffset.y / 10).toFixed(1)}mm
</div>
- <div className="offset-hint">
+ <div className="text-[10px] text-gray-600 italic">
Drag pattern to move • Drag background to pan
</div>
</div>
{/* Zoom Controls Overlay */}
- <div className="zoom-controls">
- <button className="zoom-btn" onClick={handleZoomIn} title="Zoom In">
+ <div className="absolute bottom-5 right-5 flex gap-2 items-center bg-white/95 backdrop-blur-sm px-3 py-2 rounded-lg shadow-lg z-10">
+ <button className="w-8 h-8 p-0 text-lg font-bold border border-gray-300 bg-white rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:-translate-y-0.5 hover:shadow-md hover:shadow-blue-600/30 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomIn} title="Zoom In">
+
</button>
- <span className="zoom-level">{Math.round(stageScale * 100)}%</span>
- <button className="zoom-btn" onClick={handleZoomOut} title="Zoom Out">
+ <span className="min-w-[50px] text-center text-[13px] font-semibold text-gray-900 select-none">{Math.round(stageScale * 100)}%</span>
+ <button className="w-8 h-8 p-0 text-lg font-bold border border-gray-300 bg-white rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:-translate-y-0.5 hover:shadow-md hover:shadow-blue-600/30 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomOut} title="Zoom Out">
−
</button>
- <button className="zoom-btn zoom-reset" onClick={handleZoomReset} title="Reset Zoom">
+ <button className="w-8 h-8 p-0 text-xl font-bold border border-gray-300 bg-white rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:-translate-y-0.5 hover:shadow-md hover:shadow-blue-600/30 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed ml-1" onClick={handleZoomReset} title="Reset Zoom">
⟲
</button>
</div>
diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx
index 1effe9d..2e3632c 100644
--- a/src/components/ProgressMonitor.tsx
+++ b/src/components/ProgressMonitor.tsx
@@ -85,30 +85,43 @@ export function ProgressMonitor({
block => currentStitch >= block.startStitch && currentStitch < block.endStitch
);
+ const stateIndicatorColors = {
+ idle: 'bg-blue-50 border-l-blue-600',
+ info: 'bg-blue-50 border-l-blue-600',
+ active: 'bg-yellow-50 border-l-yellow-500',
+ waiting: 'bg-yellow-50 border-l-yellow-500',
+ warning: 'bg-yellow-50 border-l-yellow-500',
+ complete: 'bg-green-50 border-l-green-600',
+ success: 'bg-green-50 border-l-green-600',
+ interrupted: 'bg-red-50 border-l-red-600',
+ error: 'bg-red-50 border-l-red-600',
+ danger: 'bg-red-50 border-l-red-600',
+ };
+
return (
- <div className="progress-panel">
- <h2>Sewing Progress</h2>
+ <div className="bg-white p-6 rounded-lg shadow-md">
+ <h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Sewing Progress</h2>
{patternInfo && (
- <div className="pattern-stats">
- <div className="detail-row">
- <span className="label">Total Stitches:</span>
- <span className="value">{patternInfo.totalStitches}</span>
+ <div>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Total Stitches:</span>
+ <span className="font-semibold">{patternInfo.totalStitches}</span>
</div>
- <div className="detail-row">
- <span className="label">Estimated Time:</span>
- <span className="value">
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Estimated Time:</span>
+ <span className="font-semibold">
{Math.floor(patternInfo.totalTime / 60)}:
{(patternInfo.totalTime % 60).toString().padStart(2, '0')}
</span>
</div>
- <div className="detail-row">
- <span className="label">Speed:</span>
- <span className="value">{patternInfo.speed} spm</span>
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Speed:</span>
+ <span className="font-semibold">{patternInfo.speed} spm</span>
</div>
- <div className="detail-row">
- <span className="label">Bounds:</span>
- <span className="value">
+ <div className="flex justify-between py-2">
+ <span className="font-medium text-gray-600">Bounds:</span>
+ <span className="font-semibold">
({patternInfo.boundLeft}, {patternInfo.boundTop}) to (
{patternInfo.boundRight}, {patternInfo.boundBottom})
</span>
@@ -117,13 +130,12 @@ export function ProgressMonitor({
)}
{colorBlocks.length > 0 && (
- <div className="color-blocks">
- <h3>Color Blocks</h3>
- <div className="color-block-list">
+ <div className="mt-6 pt-4 border-t border-gray-300">
+ <h3 className="text-base font-semibold my-4">Color Blocks</h3>
+ <div className="flex flex-col gap-2">
{colorBlocks.map((block, index) => {
const isCompleted = currentStitch >= block.endStitch;
const isCurrent = index === currentBlockIndex;
- const isPending = currentStitch < block.startStitch;
// Calculate progress within current block
let blockProgress = 0;
@@ -136,30 +148,30 @@ export function ProgressMonitor({
return (
<div
key={index}
- className={`color-block-item ${
- isCompleted ? 'completed' : isCurrent ? 'current' : 'pending'
+ className={`p-3 rounded bg-gray-100 border-2 border-transparent transition-all ${
+ isCompleted ? 'border-green-600 bg-green-50' : isCurrent ? 'border-blue-600 bg-blue-50 shadow-md shadow-blue-600/20' : 'opacity-60'
}`}
>
- <div className="block-header">
+ <div className="flex items-center gap-3">
<div
- className="color-swatch"
+ className="w-6 h-6 rounded border-2 border-gray-300 shadow-sm flex-shrink-0"
style={{ backgroundColor: block.threadHex }}
title={block.threadHex}
/>
- <span className="block-label">
+ <span className="font-semibold flex-1">
Thread {block.colorIndex + 1}
</span>
- <span className="block-status">
+ <span className={`text-xl font-bold ${isCompleted ? 'text-green-600' : isCurrent ? 'text-blue-600' : 'text-gray-600'}`}>
{isCompleted ? '✓' : isCurrent ? '→' : '○'}
</span>
- <span className="block-stitches">
+ <span className="text-sm text-gray-600">
{block.stitchCount} stitches
</span>
</div>
{isCurrent && (
- <div className="block-progress-bar">
+ <div className="mt-2 h-1 bg-white rounded overflow-hidden">
<div
- className="block-progress-fill"
+ className="h-full bg-blue-600 transition-all duration-300"
style={{ width: `${blockProgress}%` }}
/>
</div>
@@ -172,60 +184,60 @@ export function ProgressMonitor({
)}
{sewingProgress && (
- <div className="sewing-stats">
- <div className="progress-bar">
- <div className="progress-fill" style={{ width: `${progressPercent}%` }} />
+ <div className="mt-4">
+ <div className="h-3 bg-gray-300 rounded-md overflow-hidden my-4 shadow-inner relative">
+ <div className="h-full bg-gradient-to-r from-blue-600 to-blue-700 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" style={{ width: `${progressPercent}%` }} />
</div>
- <div className="detail-row">
- <span className="label">Current Stitch:</span>
- <span className="value">
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Current Stitch:</span>
+ <span className="font-semibold">
{sewingProgress.currentStitch} / {patternInfo?.totalStitches || 0}
</span>
</div>
- <div className="detail-row">
- <span className="label">Elapsed Time:</span>
- <span className="value">
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Elapsed Time:</span>
+ <span className="font-semibold">
{Math.floor(sewingProgress.currentTime / 60)}:
{(sewingProgress.currentTime % 60).toString().padStart(2, '0')}
</span>
</div>
- <div className="detail-row">
- <span className="label">Position:</span>
- <span className="value">
+ <div className="flex justify-between py-2 border-b border-gray-300">
+ <span className="font-medium text-gray-600">Position:</span>
+ <span className="font-semibold">
({(sewingProgress.positionX / 10).toFixed(1)}mm,{' '}
{(sewingProgress.positionY / 10).toFixed(1)}mm)
</span>
</div>
- <div className="detail-row">
- <span className="label">Progress:</span>
- <span className="value">{progressPercent.toFixed(1)}%</span>
+ <div className="flex justify-between py-2">
+ <span className="font-medium text-gray-600">Progress:</span>
+ <span className="font-semibold">{progressPercent.toFixed(1)}%</span>
</div>
</div>
)}
{/* State Visual Indicator */}
{patternInfo && (
- <div className={`state-indicator state-indicator-${stateVisual.color}`}>
- <span className="state-icon">{stateVisual.icon}</span>
- <div className="state-info">
- <div className="state-label">{stateVisual.label}</div>
- <div className="state-description">{stateVisual.description}</div>
+ <div className={`flex items-center gap-4 p-4 rounded-lg my-4 border-l-4 ${stateIndicatorColors[stateVisual.color as keyof typeof stateIndicatorColors] || stateIndicatorColors.info}`}>
+ <span className="text-3xl leading-none">{stateVisual.icon}</span>
+ <div className="flex-1">
+ <div className="font-semibold text-base mb-1">{stateVisual.label}</div>
+ <div className="text-sm text-gray-600">{stateVisual.description}</div>
</div>
</div>
)}
- <div className="progress-actions">
+ <div className="flex gap-3 mt-4 flex-wrap">
{/* Mask trace waiting for confirmation */}
{isMaskTraceWait && (
- <div className="status-message warning">
+ <div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded border border-yellow-200 font-medium w-full">
Press button on machine to start mask trace
</div>
)}
{/* Mask trace in progress */}
{isMaskTracing && (
- <div className="status-message info">
+ <div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Mask trace in progress...
</div>
)}
@@ -233,16 +245,16 @@ export function ProgressMonitor({
{/* Mask trace complete - ready to sew */}
{isMaskTraceComplete && (
<>
- <div className="status-message success">
+ <div className="bg-green-100 text-green-800 px-4 py-3 rounded border border-green-200 font-medium w-full">
Mask trace complete!
</div>
{canStartSewing(machineStatus) && (
- <button onClick={onStartSewing} className="btn-primary">
+ <button onClick={onStartSewing} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Sewing
</button>
)}
{canStartMaskTrace(machineStatus) && (
- <button onClick={onStartMaskTrace} className="btn-secondary">
+ <button onClick={onStartMaskTrace} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Trace Again
</button>
)}
@@ -252,11 +264,11 @@ export function ProgressMonitor({
{/* Pattern uploaded, ready to trace */}
{machineStatus === MachineStatus.IDLE && (
<>
- <div className="status-message info">
+ <div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Pattern uploaded successfully
</div>
{canStartMaskTrace(machineStatus) && (
- <button onClick={onStartMaskTrace} className="btn-secondary">
+ <button onClick={onStartMaskTrace} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Mask Trace
</button>
)}
@@ -267,12 +279,12 @@ export function ProgressMonitor({
{machineStatus === MachineStatus.SEWING_WAIT && (
<>
{canStartMaskTrace(machineStatus) && (
- <button onClick={onStartMaskTrace} className="btn-secondary">
+ <button onClick={onStartMaskTrace} className="px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm hover:bg-gray-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Mask Trace
</button>
)}
{canStartSewing(machineStatus) && (
- <button onClick={onStartSewing} className="btn-primary">
+ <button onClick={onStartSewing} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Start Sewing
</button>
)}
@@ -281,42 +293,42 @@ export function ProgressMonitor({
{/* Resume sewing for interrupted states */}
{canResumeSewing(machineStatus) && (
- <button onClick={onResumeSewing} className="btn-primary">
+ <button onClick={onResumeSewing} className="px-6 py-3 bg-blue-600 text-white rounded font-semibold text-sm hover:bg-blue-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
▶️ Resume Sewing
</button>
)}
{/* Color change needed */}
{isColorChange && (
- <div className="status-message warning">
+ <div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded border border-yellow-200 font-medium w-full">
Waiting for color change - change thread and press button on machine
</div>
)}
{/* Sewing in progress */}
{isSewing && (
- <div className="status-message info">
+ <div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Sewing in progress...
</div>
)}
{/* Sewing complete */}
{isComplete && (
- <div className="status-message success">
+ <div className="bg-green-100 text-green-800 px-4 py-3 rounded border border-green-200 font-medium w-full">
Sewing complete!
</div>
)}
{/* Delete pattern button - ONLY show when safe */}
{patternInfo && canDeletePattern(machineStatus) && (
- <button onClick={onDeletePattern} className="btn-danger">
+ <button onClick={onDeletePattern} className="px-6 py-3 bg-red-600 text-white rounded font-semibold text-sm hover:bg-red-700 transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 disabled:opacity-50 disabled:cursor-not-allowed disabled:grayscale-[0.3]">
Delete Pattern
</button>
)}
{/* Show warning when delete is unavailable */}
{patternInfo && !canDeletePattern(machineStatus) && (
- <div className="status-message info">
+ <div className="bg-cyan-100 text-cyan-800 px-4 py-3 rounded border border-cyan-200 font-medium w-full">
Pattern cannot be deleted during active operations
</div>
)}
diff --git a/src/index.css b/src/index.css
index ec2585e..1cd48aa 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5,6 +5,8 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ background-color: #f5f5f5;
+ color: #212529;
}
code {
diff --git a/vite.config.ts b/vite.config.ts
index 8fb08db..3dda9e2 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,5 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
@@ -24,7 +25,7 @@ function viteStaticCopyPyodide() {
// https://vite.dev/config/
export default defineConfig({
- plugins: [react(), viteStaticCopyPyodide()],
+ plugins: [react(), tailwindcss(), viteStaticCopyPyodide()],
optimizeDeps: {
exclude: ['pyodide'],
},
Served by rngit 1.4.2 - Generated in 0.24s